import math
import sys
import aiohttp
import asyncio
import datetime
from typing import Union, Annotated, List, Tuple, Dict
from fastapi import APIRouter, Query
from pydantic import BaseModel
import psycopg2
import collections
from geopy.distance import geodesic
import numpy as np
import multiprocessing

from log.logger import logger_time

router = APIRouter(prefix="/speed_interval")


class DockedTime:
    def __init__(self, host="119.3.248.125", port="21632", user="pg", password="Hello1234", database="route_labeling"):
        self.host = host
        self.port = port
        self.user = user
        self.password = password
        self.database = database
        self.port_list = None
        self.idx2port = None
        self.start_lon2idx = None
        self.start_lat2idx = None
        self.start_latlon2idx = None
        self.merge_times = 0

        self.update_data()

    def get_data(self):
        """
        获取数据库中的港口数据
        :return:
        """
        conn = psycopg2.connect(database=self.database, user=self.user, password=self.password, host=self.host, port=self.port)
        cursor = conn.cursor()
        sql = "select port_id, name, lat, lon from port_info;"
        cursor.execute(sql)
        self.port_list = cursor.fetchall()
        self.idx2port = {t[0]: list(t[1:]) for t in self.port_list}
        # sql = "select port_id, lat, lon from port_info order by lat;"
        # cursor.execute(sql)
        # self.sorted_port = cursor.fetchall()
        conn.close()

    @staticmethod
    def lat_idx(x) -> int:
        """
        前两种做索引的方式中,,纬度到索引的映射
        :param x: 经度
        :return: idx
        """
        return math.floor((x + 90) * 50)

    @staticmethod
    def lon_idx(x) -> int:
        """
        前两种做索引的方式中,经度到索引的映射
        :param x: 纬度
        :return: idx
        """
        return math.floor((x + 180) * 10)

    @staticmethod
    def lon_idx_with_lat(lon, lat) -> int:
        """
        第3种做索引的方式中,根据纬度来动态地规划经度索引的粒度
        :param lon: 纬度
        :param lat: 经度
        :return: idx
        """
        if abs(lat) <= 33:
            return math.floor((lon + 180) * 50)
        elif abs(lat) <= 70:
            return math.floor((lon + 180) * 20)
        elif abs(lat) <= 80:
            return math.floor((lon + 180) * 20)
        else:
            return 0

    def make_index_v1(self):
        """
        纬度每0.02划分一个网格
        从[-90,-89.98)到[89.98,90],一共是9000个区间
        做区间起点(做一下处理,lambda x: (x+90)*50,因此是0到8999)到port_id的列表的一个字典
        经度每0.1划分一个网格
        从[-180,-179.9)到[179.9,180],一共是3600个区间
        做区间起点(做一下处理,lambda x: (x+180)*10,因此是0到3599)到port_id的列表的一个字典
        :return:
        """
        self.start_lat2idx = collections.defaultdict(list)
        self.start_lon2idx = collections.defaultdict(list)
        for info in self.port_list:
            port_id, name, lat, lon = info
            start_lat = self.lat_idx(lat)
            start_lon = self.lon_idx(lon)
            self.start_lat2idx[start_lat].append(port_id)
            self.start_lon2idx[start_lon].append(port_id)

    def make_index_v2(self):
        """
        网格的粒度和v1是一样的,不过这里是复合的,而不是两个分开
        :return:
        """
        self.start_latlon2idx = collections.defaultdict(lambda: collections.defaultdict(list))
        for info in self.port_list:
            port_id, name, lat, lon = info
            start_lat = self.lat_idx(lat)
            start_lon = self.lon_idx(lon)
            self.start_latlon2idx[start_lat][start_lon].append(port_id)

    def make_index_v3(self):
        """
        纬度的划分还是0.02
        经度的划分按照纬度的不同细分粒度
        |lat|<=33的时候,粒度是0.02, 33<|lat|<=70用0.05的粒度, 70<|lat|<=80, 用0.1的粒度, |lat|>80的仅有一档,就相当于是只按纬度划分
        (这些都是建立在1nm的基础上)
        :return:
        """
        self.merge_times = 0
        self.start_latlon2idx = collections.defaultdict(lambda: collections.defaultdict(list))
        for info in self.port_list:
            port_id, name, lat, lon = info
            start_lat = self.lat_idx(lat)
            start_lon = self.lon_idx_with_lat(lon, lat)
            l = self.start_latlon2idx[start_lat][start_lon]
            flag = 1
            for p_id in l:
                la, lo = self.idx2port[p_id][1:]
                if geodesic((la, lo), (lat, lon)) < 0.1:
                    self.idx2port[p_id][0] += ";" + name
                    self.merge_times += 1
                    flag = 0
                    break
            if flag:
                l.append(port_id)

    def update_data(self):
        """
        从数据库中获取数据并且建立索引
        :return:
        """
        self.get_data()
        # self.make_index_v1()
        # self.make_index_v2()
        self.make_index_v3()
        logger_time.info(f"update port data from database, merge times: {self.merge_times}, ports after merging: {len(self.port_list)-self.merge_times}")

    def get_closest(self, lat: float, lon: float) -> Union[int, None]:
        """
        查找1海里内的最近的港口
        :param lat: 纬度
        :param lon: 经度
        :return: 距离指定经纬度处最近的港口
        """
        start_lat = self.lat_idx(lat)
        start_lon = self.lon_idx(lon)

        # # 对应index_v1
        # candidate_lat = set(self.start_lat2idx[start_lat-1] + self.start_lat2idx[start_lat] + self.start_lat2idx[start_lat+1])
        # candidate_lon = set(self.start_lon2idx[start_lon-1] + self.start_lon2idx[start_lon] + self.start_lon2idx[start_lon+1])
        # candidates = candidate_lon & candidate_lat

        # # 对应index_v2
        # candidates = []
        # for la in range(start_lat-1, start_lat+2):
        #     for lo in range(start_lon-1, start_lon+2):
        #         candidates += self.start_latlon2idx[la][lo]

        # 对应index_v3
        candidates = []
        start_lon = self.lon_idx_with_lat(lon, lat)
        for la in range(start_lat-1, start_lat+2):
            for lo in range(start_lon-1, start_lon+2):
                candidates += self.start_latlon2idx[la][lo]

        if candidates:
            candidates_with_distance = [(c, geodesic((self.idx2port[c][1], self.idx2port[c][2]), (lat, lon)).nm) for c in candidates]
            candidates_with_distance.sort(key=lambda x: x[1])
            # # 测试用,备用港口是否过多
            # if len(candidates) > 3:
            #     print(len(candidates), candidates_with_distance)
            if candidates_with_distance[0][1] < 1:
                return candidates_with_distance[0][0]
        else:
            return None

    async def get_docker_info(self, info_list: List[Tuple], start_end: Tuple[int, int] = (0, -1)) -> Dict[int, float]:
        """
        获取在各个港口处停留的时间
        :param info_list: 航行信息列表
        :param start_end: (起始idx, 结尾idx)
        :return: {港口id: 在港口附近的总秒数}
        """
        # 两次报告都在某国港口1海里以内的话,期间的时间视为在该港口停留
        # 结果是port_id: total_seconds的字典
        result = collections.defaultdict(int)
        n = len(info_list)
        start, end = start_end[0], start_end[1]
        if start < 0 or start > n-1:
            start = 0
        if end < 0 or end > n-1:
            end = n - 1
        if end - start <= 0:
            return result
        prev = self.get_closest(info_list[start][1], info_list[start][2])
        start_time = info_list[start][0]
        for info in info_list[start+1:end+1]:
            end_time, lat, lon = info
            # # 启发式,粘性归类,先看看是否和上次的港口距离不到1
            # if not prev:
            #     port_id = self.get_closest(lat, lon)
            # else:
            #     la, lo = self.idx2port[prev][1:]
            #     if geodesic((lat, lon), (la, lo)).nm < 1:
            #         result[port_id] += (end_time - start_time).total_seconds()
            #     else:
            #         port_id = self.get_closest(lat, lon)
            #         if port_id and prev == port_id:
            #             result[port_id] += (end_time - start_time).total_seconds()
            port_id = self.get_closest(lat, lon)
            if port_id and prev == port_id:
                result[port_id] += (end_time - start_time).total_seconds()
            prev = port_id
            start_time = end_time
        return result

    async def get_docker_info_parallel(self, info_list: List[Tuple], workers: int = 1) -> Dict[int, float]:
        """
        并发计算(其实无意义)
        :param info_list: 航行信息
        :param workers: 并发数
        :return:
        """
        n = len(info_list)
        sep = [int(x) for x in np.linspace(start=0, stop=n-1, num=workers+1)]
        start_idx = sep[:-1]
        end_idx = sep[1:]
        coroutines = []
        result = collections.defaultdict(int)
        for s, e in zip(start_idx, end_idx):
            coroutines.append(asyncio.create_task(self.get_docker_info(info_list[s: e])))
        results = await asyncio.gather(*coroutines)
        for r in results:
            for k, v in r.items():
                result[k] += v
        # for coroutine in coroutines:
        #     r = await coroutine
        #     for k, v in r.items():
        #         result[k] += v
        return result

    async def get_docker_info_format(self, info_list: List[Tuple]) -> List[Dict]:
        """
        转换为需要的格式
        :param info_list: (时间, 纬度, 经度) 三元组的列表, 类型是datetime, float, float
        :return:
        """
        d = await self.get_docker_info(info_list)
        # if len(info_list) <= 1000:
        #     d = await self.get_docker_info()
        # else:
        #     d = await self.get_docker_info_parallel()
        result = []
        for port_id in d.keys():
            temp = {}
            name, lat, lon = self.idx2port[port_id]
            temp.update(portId=port_id, portName=name, lat=lat, lon=lon, totalSeconds=d[port_id])
            result.append(temp)
        return result

